React Hooks(四):全函數式React
2019-09-09
筆者在上年十一月React Hooks剛發佈時,就寫過關於React Hooks的應用,如何簡化開發React 應用時要寫的程式碼,之後又介紹了Redux-React-Hooks
這個筆者認為有不錯前途的組件,雖然隨著React-Redux
加入了React Hooks的應用,現在寫React + Redux應用,已無需再寫長長的mapStateToProps
及mapDispatchToProps
。
但有一個不解之謎,就是為何只要加入React Hooks,要寫的程式碼就會大大簡化呢?縱使軟件工程師都知道框架及程式庫可以減少程式碼之複雜性(Complexity),但大家都明白這個世界是沒有免費午餐(No Free Lunch)的道理,那使用 React Hooks又有何代價呢?
要找到答案,最佳方法方法莫過於觀察實際例子。
以下是筆者在堂上所寫的一個TodoList的例子,這個例子以React + TypeScript + Redux + React Redux
寫成,筆者一共寫了兩個實作,一個用傳統的類別式部件(Class Component) 去寫,姑且名之為例子A;一種以新式React Hooks
加上函數式部件(Function Component)去寫,名之為例子B。為了篇幅所限,組件不少程式碼已省略。
例子A:類別式部件
以TypeScript使用類別式部件,需要為props
及state
定義接口(interface):ITodoProps
及ITodoState
。
interface ITodoProps{ todos: ITodo[] list:(completed?:boolean)=>void create:(task:string)=>void } interface ITodoState{ task: string } class TodoList extends React.Component<ITodoProps,ITodoState>{ constructor(props:ITodoProps){ super(props); this.state = { task:"" } } /** * Private methods here */ public componentDidMount(){ this.props.list(); } public render(){ return ( <div> {/* UI logic here*/} </div> ) } } const mapStateToProps = (state:IRootState)=>{ return { todos: state.todos.todos } } const mapDispatchToProps = (dispatch:ThunkDispatch)=>{ return { list: (completed?:boolean)=> dispatch(list(completed)), create:(task:string)=>dispatch(create(task)), } } export default connect(mapStateToProps,mapDispatchToProps)(TodoList);
為何需要定義這兩個接口?因為使用React 加上TypeScript時,React需要清晰判斷該部件之props
及state
有何性質(attributes)及方法(methods),才能夠讓程式碼編輯器(Code Editor)去判斷是否有型別錯誤(Type error). ITodoProps
及ITodoState
為React部件提供了型別參數(Type Parameter),就如程式碼中所示React.Component<ITodoProps,ITodoState>
。
底下的mapStateToProps
及mapDispatchToProps
亦如常定義,再用connect
將這兩個映射(Mapping)用在部件之上。
const mapStateToProps = (state:IRootState)=>{ return { todos: state.todos.todos } } const mapDispatchToProps = (dispatch:ThunkDispatch)=>{ return { list: (completed?:boolean)=> dispatch(list(completed)), create:(task:string)=>dispatch(create(task)), } } export default connect(mapStateToProps,mapDispatchToProps)(TodoList);
例子B: 函數式部件 + React Hooks
這是使用 React Hooks的程式碼,使用react-redux新加入的React Hooks功能,可以直接使用 useSelector
去從Redux State中讀取state。又可以使用 useDispatch
直接得到dispatch
函數。,要定義local state
只需要使用 useState
去定義。因為 TypeScript能夠從useState("")
中自動推論型別,因此省略了接口的定義。更無須mapStateToProps
及mapDispatchToProps
,連connect
也一併省略,結果出來就簡潔不少。
export function TodoListHooks(props:{}){ const [task,setTask] = useState(""); const todos = useSelector((state:IRootState)=>state.todos.todos); const dispatch = useDispatch<ThunkDispatch>(); useEffect(()=>{ dispatch(list()); },[]) /* Handler functions here */ return ( <div> {/* UI logic here*/} </div> ) }
取代類別式部件之React Hooks
由上面例子可見,React Hooks令普通的函數式部件(Function Component)可以運用useState
及useEffect
,處理State及Side Effect. 因此,使用React Hooks加上函數式部件是可以完全取代類別式部件(Class Component)。再加上react-redux
,更可以靈活運用Redux
中的狀態。
筆者認為,究其原因,React Hooks之所以能夠大大減少程式碼量,是因為React Hooks + 函數式組件 與React 函數式編程(Functional Programming)的概念非常切合。可以說是為React推廣函數式編程上,加上一直從缺之一步。
React為前端世界帶來了很多函數式編程,例如Immutable Data(不變數據)、Redux中的Reducer、Referential Transparency(參照透明性),都是由React首先帶來前端世界。不過React 當中有一個一直與函數式編程不契合的地方,就是React中的類別式組件
(Class Component)。大家如果用過任何函數式編程語言(Functional Programming Languages),例如Haskell
、Standard ML
,甚至乎近年出現的ReasonML
,都會發現類別其實在編程語言之中,不是必要的組成部份,最長壽的C語言就一直都沒有class
的蹤跡。
React之所以在一直強調函數式編程的背景下,依然加入類別式組件,是為了利用class
的this
,去為組件儲存local state
。就如上面的TodoList
例子一樣。在constructor
就先為local state
的task
加上了一個 空白字串的數值。
constructor(props:ITodoProps){ super(props); this.state = { task:"" } }
可是加入了React Hooks
之後,因為有了儲存state
的機制,就無須再為了利用this
的概念,而再使用類別式組件。以函數式組件取代類別式組件,除了減少程式碼長度外,可見有兩個即時的好處。
降低初學者難度
各位讀者可能已有多年編程經驗,覺得使用類別如呼吸空氣一樣自然,但由筆者教學所見,其實理解類別,所需要的思維負擔(Mental Burden),比理解函數要多得多。
重用以上TodoList的例子:
初學者要記緊首先運行的是constructor
,然後是componentDidMount
,最後運行的才是render
,如果加入Redux
的概念,就更要有多次render
,同時也要緊記不能在render
裏面setState
,以免進入無限迴圈。
相對之下,函數的例子由上而下,一目了然,也較符合人類的直覺。對初學者而言,較易掌握。
而且對初學React的人而言,要記住兩種寫法(函數式及類別式),一定比只記住一種寫法要難。
寫測試的難度
React 的官方測試配件是Jest,要為類別式組件寫單元測試(Unit Test case),殊不簡單,因為react-redux
的connect難以測試,往往坊間的教學都會提議開發者export 沒有加上connect的組件直接測試。避免測試加上Redux的組件。
interface ITodoProps{ todos: ITodo[] list:(completed?:boolean)=>void create:(task:string)=>void } interface ITodoState{ task: string } // 直接測試沒有Redux的TodoList export class TodoList extends React.Component<ITodoProps,ITodoState>{ ... } ... // 這個太難寫單元測試.. 所以不寫測試 export default connect(mapStateToProps,mapDispatchToProps)(TodoList);
相較之下,要為函數式組件+ React Hooks + React-Redux寫單元測試卻很簡單,只需要將useSelector
及useDispatch
用jest
mock好了就可以。
// 可以整個組件一起測試 export function TodoListHooks(props:{}){ const [task,setTask] = useState(""); // 用jest.mock 替代 useSelector const todos = useSelector((state:IRootState)=>state.todos.todos); // 用jest.mock 替代 useDispatch const dispatch = useDispatch<ThunkDispatch>(); useEffect(()=>{ dispatch(list()); },[]) /* Handler functions here */ return ( <div> {/* UI logic here*/} </div> ) }
函數式組件的難處
由上文可見,React Hooks可以令開發者以無類別式組件(Class Component Free)的方式去寫React應用,整個React都是由函數式組件所組成。 當然正如第一段所言,世上沒有免費午餐,用React Hooks + 函數式組件的代價,就是開發者必須對閉包(Closure)非常熟練,因為整個React Hooks原理上正是基於 JavaScript的closure。正如以下一段程式碼:
const dispatch = useDispatch<ThunkDispatch>(); useEffect(()=>{ dispatch(list()); },[])
useEffect中呼叫了dispatch
函數,而dispatch
函數是定義於useEffect
裏面函數的可視範圍之外(Out of Scope),這段程式碼之所以能夠正常運作,正是因為JavaScript將外邊個dispatch
「包」到裏面的函數以作後用,因此裏面的函數可以讀取dispatch
作呼叫之用,這就是閉包的基本概念。
總結
React Hooks從發佈至今約十個月,正式推出至今約半年,愈來愈多的程式庫開始引入React Hooks
作為預設使用方法。而且亦補上了React全函數式組件欠缺的一環,真正成為全函數式React(React with Full Functional Programming)了。
留言
閱讀更多
到底React Hooks 有何特別?
2018-11-27
新近推出的React 16.7包括一個很有趣的功能,名字叫做React Hooks。看到這個名字,很多人會下意識認為是在講componentDidMount, componentDidUpdate等方法。但其實這些方法的正名是 React Lifecycle Method, 推出React Hooks是為了方便開發者多用functional component,但仍然能夠使用state及 props等重要功能。
到底React Hooks有何特別(二)?淺談useEffect及useReducer
2018-11-29
於本篇文章的上集,我們討論了useState如何令Stateful React Component簡化良多,此篇主要討論的是如何使 用useEffect。useEffect可以簡化state,很多人都提到React Hooks有可能可以完全取代Redux作為 React State Management的標準,正因如此。
React Hooks(三):Redux-React-Hook
2019-03-27
React Hooks在React 16.8.0的版本正式成為React的正式功能。正如前兩篇所言,React Hooks簡化了寫複雜代碼的難度,亦令React的函數式部件(Functional Components)亦能使用state及props,可是傳說中React Hooks將會取代Redux呢?卻一直都是只聞樓梯響。這篇文章就會介紹一個筆者認為頗有前景的組件,就是在Github中的facebookincubator中的redux-react-hook。